/**
 * file name:		SwapByteArray.java
 * copyright:		© 2009 Kingsoft
 * author:			TanMenglong
 * version:			?.??
 * created:			2009-08-10
 * last modified:	2009-08-10
 * description:		HERE
 */

package buffer;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

/**
 * byte array using both memory and disk.
 * @author TanMenglong
 *
 */
public class SwapByteArray {
	protected static final long DEFAULT_BLOCK_SIZE = 2048L;
	protected static final int BUFF_SIZE = 1024;

	protected long blockSize = 0;		// block size
	protected long size = 0;			// point to add block

	protected HashMap<Long, StorageBlock> blockMap = new HashMap<Long, StorageBlock>();
	protected ArrayList<StorageBlock> blockList = new ArrayList<StorageBlock>();

	public SwapByteArray() {
		this(DEFAULT_BLOCK_SIZE);
	}
	
	public SwapByteArray(long blockSize) {
		this.blockSize = blockSize;
	}
	
	public void addBlock(BlockType type) throws UnknownBlockTypeException, IOException {
		StorageBlock block;
		try {
			block = getBlockByTypeBlock(type);
		} catch (UnknownBlockTypeException e) {
			throw new UnknownBlockTypeException(e);
		}
		
		block.allocate();
		
		this.size += this.blockSize;
		blockList.add(block);
	}
	
	public void release() throws IOException {
		Iterator<StorageBlock> iterator = blockList.iterator();
		while (iterator.hasNext()) {
			iterator.next().release();
		}
	}
	
	public int read(long pos) throws IndexOutOfBoundsException, IOException, IllegalArgumentException {
		if (pos < 0) {
			throw new IllegalArgumentException();
		}

		if (pos < 0 || pos >= this.size) {
			throw new IndexOutOfBoundsException();
		}
		
		int blockIdx = (int)(pos / blockSize);
		long offset = pos % blockSize;

		StorageBlock block = this.blockList.get(blockIdx);

		return block.read(offset);
	}
	
	public int write(int data, long pos) throws IndexOutOfBoundsException, IllegalArgumentException, IOException {
		if (pos < 0) {
			throw new IllegalArgumentException();
		}

		if (pos < 0 || pos >= this.size) {
			throw new IndexOutOfBoundsException();
		}
		
		int blockIdx = (int)(pos / blockSize);
		long offset = pos % blockSize;

		StorageBlock block = this.blockList.get(blockIdx);
		
		return block.write(data, offset);
	}
	
	public long getSize() {
		return this.size;
	}
	
	protected StorageBlock getBlockByTypeBlock(BlockType type) throws UnknownBlockTypeException {
		StorageBlock block = null;
		switch (type) {
		case MEM:
			block = new MemStorageBlock();
			break;
		case DISK:
			block = new DiskStorageBlock();
			break;
		default:
			throw new UnknownBlockTypeException();
		}
		return block;
	}
	
	/**
	 * Storage block type defination.
	 * @author TanMenglong
	 *
	 */
	public enum BlockType {MEM, DISK}

	/**
	 * Abstract base class StorageBlock.
	 * @author TanMenglong
	 *
	 */
	protected abstract static class StorageBlock {
		protected long size = 0;
		
		protected long getSize() {return this.size;}
		protected abstract BlockType getType();
		
		protected abstract void allocate() throws IOException;
		protected abstract void release() throws IOException;
		
		protected abstract int read(long pos) 
			throws IndexOutOfBoundsException, IOException, IllegalArgumentException;
		protected abstract int read(byte[] b, long pos) 
			throws IndexOutOfBoundsException, IOException, IllegalArgumentException;
		protected abstract int read(byte[] b, long pos, int len) 
			throws IndexOutOfBoundsException, IOException, IllegalArgumentException;

		protected abstract int write(int data, long pos) 
			throws IndexOutOfBoundsException, IOException, IllegalArgumentException;
		protected abstract int write(byte[] b, long pos) 
			throws IndexOutOfBoundsException, IOException, IllegalArgumentException;
		protected abstract int write(byte[] b, long pos, int len) 
			throws IndexOutOfBoundsException, IOException, IllegalArgumentException;
	}
	
	/**
	 * Memory block.
	 * @author TanMenglong
	 *
	 */
	protected class MemStorageBlock extends StorageBlock {
		protected byte[] buff = null;
		
		@Override
		protected BlockType getType() {
			return BlockType.MEM;
		}

		@Override
		protected void allocate() throws IOException {
			try {
				this.buff = new byte[(int)blockSize];
			} catch (OutOfMemoryError e) {
				throw new OutOfMemoryError("out of heap.");
			}
			this.size = blockSize;
		}

		@Override
		protected void release() throws IOException {
			this.buff = null;
		}

		@Override
		protected int read(long pos) throws IndexOutOfBoundsException, IOException, IllegalArgumentException {
			if (pos < 0) {
				throw new IllegalArgumentException();
			}

			if (pos < 0 || pos >= this.buff.length) {
				throw new IndexOutOfBoundsException();
			}
			
			return this.buff[(int)pos];
		}

		@Override
		protected int read(byte[] b, long pos) 
				throws IndexOutOfBoundsException, IOException, IllegalArgumentException {
			read(b, pos, b.length);
			return b.length;
		}

		@Override
		protected int read(byte[] b, long pos, int len) 
				throws IndexOutOfBoundsException, IOException, IllegalArgumentException {

			if (null == b || pos < 0 || len < 0) {
				throw new IllegalArgumentException();
			}
			
			if (pos < 0 || pos >= this.size || b.length < len || pos + b.length > this.size) {
				throw new IndexOutOfBoundsException();
			}
			
			System.arraycopy(this.buff, (int)pos, b, 0, len);
			return len;
		}

		@Override
		protected int write(int data, long pos)
				throws IndexOutOfBoundsException, IOException, IllegalArgumentException {
			if (pos < 0) {
				throw new IllegalArgumentException();
			}

			if (pos < 0 || pos >= this.buff.length) {
				throw new IndexOutOfBoundsException();
			}
			
			this.buff[(int)pos] = (byte)data;

			return 0x01; // one byte
		}

		@Override
		protected int write(byte[] b, long pos)
				throws IndexOutOfBoundsException, IOException, IllegalArgumentException {
			write(b, pos, b.length);
			return b.length;
		}

		@Override
		protected int write(byte[] b, long pos, int len)
				throws IndexOutOfBoundsException, IOException, IllegalArgumentException {
			if (null == b || pos < 0 || len < 0) {
				throw new IllegalArgumentException();
			}
			
			if (pos < 0 || pos >= this.size || b.length < len || pos + b.length > this.size) {
				throw new IndexOutOfBoundsException();
			}
			
			System.arraycopy(b, 0, this.buff, (int)pos, len);
			return len;
		}

	}

	/**
	 * Disk block.
	 * @author TanMenglong
	 *
	 */
	protected class DiskStorageBlock extends StorageBlock {
		protected File file = null;
		protected RandomAccessFile fileAccessor = null;
		
		@Override
		protected BlockType getType() {
			return BlockType.DISK;
		}

		@Override
		protected void allocate() throws IOException {
			this.file = File.createTempFile("kbuff_", "tmp");
			this.file.deleteOnExit();
			this.fileAccessor = new RandomAccessFile(this.file, "rw");
			
			// fill 0x00 data
			byte[] emptyData = new byte[BUFF_SIZE];
			try {
				for (int i = 0; i < blockSize / BUFF_SIZE; ++i) {
					fileAccessor.write(emptyData);
				}
				for (int i = 0; i < blockSize % BUFF_SIZE; ++i) {
					fileAccessor.write(0x00);
				}
			} catch (IOException e) {
				this.fileAccessor.close();
				throw e;
			}
			this.size = blockSize;
		}

		@Override
		protected void release() throws IOException {
			this.fileAccessor.close();
		}

		@Override
		protected int read(long pos) throws IndexOutOfBoundsException, IOException, IllegalArgumentException {
			if (pos < 0) {
				throw new IllegalArgumentException();
			}

			if (pos < 0 || pos >= blockSize) {
				throw new IndexOutOfBoundsException();
			}
			
			synchronized (fileAccessor) {
				// FIXME: pos vs offset
				this.fileAccessor.seek(pos);
				return this.fileAccessor.read();
			}
		}

		@Override
		protected int read(byte[] b, long pos) throws IndexOutOfBoundsException,
				IOException, IllegalArgumentException {
			read(b, pos, b.length);
			return b.length;
		}

		@Override
		protected int read(byte[] b, long pos, int len) 
			throws IndexOutOfBoundsException, IOException, IllegalArgumentException {

			if (null == b || pos < 0 || len < 0) {
				throw new IllegalArgumentException();
			}
			
			if (pos < 0 || pos >= this.size || b.length < len || pos + b.length > this.size) {
				throw new IndexOutOfBoundsException();
			}
			
			synchronized (fileAccessor) {
				this.fileAccessor.seek(pos);
				this.fileAccessor.read(b, 0, len);
			}
			return len;
		}

		@Override
		protected int write(int data, long pos)
				throws IndexOutOfBoundsException, IOException, IllegalArgumentException {
			if (pos < 0) {
				throw new IllegalArgumentException();
			}

			if (pos < 0 || pos >= blockSize) {
				throw new IndexOutOfBoundsException();
			}
			
			synchronized (fileAccessor) {
				// FIXME: pos vs offset
				this.fileAccessor.seek(pos);
				this.fileAccessor.write(data);
			}
			return 0x01; // one byte
		}

		@Override
		protected int write(byte[] b, long pos)
				throws IndexOutOfBoundsException, IOException, IllegalArgumentException {
			write(b, pos, b.length);
			return b.length;
		}

		@Override
		protected int write(byte[] b, long pos, int len)
				throws IndexOutOfBoundsException, IOException, IllegalArgumentException {
			if (null == b || pos < 0 || len < 0) {
				throw new IllegalArgumentException();
			}
			
			if (pos < 0 || pos >= this.size || b.length < len || pos + b.length > this.size) {
				throw new IndexOutOfBoundsException();
			}

			synchronized (fileAccessor) {
				if (pos > Integer.MAX_VALUE) {
					this.fileAccessor.seek(pos);
					this.fileAccessor.write(b, 0, len);
				}
			}
			return len;
		}
		
	}
	
	/**
	 * UnknownBlockTypeException.
	 * @author TanMenglong
	 *
	 */
	public static class UnknownBlockTypeException extends RuntimeException {
		private static final long serialVersionUID = 9007541143198106299L;
		public UnknownBlockTypeException() {super();}
		public UnknownBlockTypeException(Throwable e) {super(e);}
		public UnknownBlockTypeException(String msg) {super(msg);}
	}
}
